secenvY 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- secenvY/__init__.py +43 -0
- secenvY/agents/__init__.py +17 -0
- secenvY/agents/dqn_agent.py +225 -0
- secenvY/agents/networks.py +81 -0
- secenvY/agents/replay_buffer.py +205 -0
- secenvY/core/__init__.py +109 -0
- secenvY/core/exceptions.py +41 -0
- secenvY/core/schemas.py +489 -0
- secenvY/core/vocabularies.py +106 -0
- secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/data_level0.bin +0 -0
- secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/header.bin +0 -0
- secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/length.bin +0 -0
- secenvY/data/chroma/1d89f56b-3cb2-42ed-bd6f-d7f3dbf9081f/link_lists.bin +0 -0
- secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/data_level0.bin +0 -0
- secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/header.bin +0 -0
- secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/length.bin +0 -0
- secenvY/data/chroma/6f46a7d2-742d-49db-8b79-8a54d292c4cd/link_lists.bin +0 -0
- secenvY/data/chroma/chroma.sqlite3 +0 -0
- secenvY/data/knowledge/components.txt +61 -0
- secenvY/data/knowledge/crypto.txt +27 -0
- secenvY/data/knowledge/permissions.txt +64 -0
- secenvY/data/knowledge/secrets.txt +76 -0
- secenvY/data/knowledge/security_basics.txt +19 -0
- secenvY/data/knowledge/storage.txt +66 -0
- secenvY/data/knowledge/webview.txt +28 -0
- secenvY/data/report.json +421 -0
- secenvY/data/sample_observation.json +333 -0
- secenvY/env/__init__.py +5 -0
- secenvY/env/security_env.py +282 -0
- secenvY/gateway/__init__.py +11 -0
- secenvY/gateway/base.py +59 -0
- secenvY/gateway/mock_gateway.py +454 -0
- secenvY/gateway/websocket_gateway.py +288 -0
- secenvY/services/__init__.py +26 -0
- secenvY/services/action_mapper.py +119 -0
- secenvY/services/episode_policy.py +74 -0
- secenvY/services/reward_calculator.py +125 -0
- secenvY/services/security_reasoner/__init__.py +46 -0
- secenvY/services/security_reasoner/build_knowledge.py +60 -0
- secenvY/services/security_reasoner/chunker.py +23 -0
- secenvY/services/security_reasoner/config.py +53 -0
- secenvY/services/security_reasoner/embeddings.py +18 -0
- secenvY/services/security_reasoner/llm.py +102 -0
- secenvY/services/security_reasoner/main.py +133 -0
- secenvY/services/security_reasoner/rag.py +79 -0
- secenvY/services/security_reasoner/schemas.py +9 -0
- secenvY/services/security_reasoner/service.py +288 -0
- secenvY/services/security_reasoner/vector_store.py +58 -0
- secenvY/services/security_reasoner_main.py +43 -0
- secenvY/services/state_builder.py +919 -0
- secenvY/services/state_builder_main.py +77 -0
- secenvY/services/state_encoder.py +379 -0
- secenvY/services/state_encoder_main.py +84 -0
- secenvY/services/state_validator.py +267 -0
- secenvY/services/state_validator_main.py +59 -0
- secenvY/services/tensor_converter.py +99 -0
- secenvy-0.1.0.dist-info/METADATA +129 -0
- secenvy-0.1.0.dist-info/RECORD +60 -0
- secenvy-0.1.0.dist-info/WHEEL +5 -0
- secenvy-0.1.0.dist-info/top_level.txt +1 -0
secenvY/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Android Security Analyzer Gymnasium Environment & Agent (SecEnv)."""
|
|
2
|
+
|
|
3
|
+
import gymnasium as gym
|
|
4
|
+
|
|
5
|
+
# Register standard AndroidSecurityEnv-v0 environment
|
|
6
|
+
gym.register(
|
|
7
|
+
id="AndroidSecurityEnv-v0",
|
|
8
|
+
entry_point="secenvY.env.security_env:SecurityEnv",
|
|
9
|
+
max_episode_steps=100,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from secenvY.agents.dqn_agent import DuelingDQNAgent
|
|
13
|
+
from secenvY.agents.networks import DuelingQNetwork
|
|
14
|
+
from secenvY.agents.replay_buffer import PrioritizedReplayBuffer, UniformReplayBuffer
|
|
15
|
+
from secenvY.env.security_env import SecurityEnv
|
|
16
|
+
from secenvY.gateway.base import SystemGateway
|
|
17
|
+
from secenvY.gateway.mock_gateway import MockSystemGateway
|
|
18
|
+
from secenvY.gateway.websocket_gateway import WebSocketSystemGateway
|
|
19
|
+
from secenvY.services.action_mapper import ActionMapper
|
|
20
|
+
from secenvY.services.episode_policy import EpisodePolicy
|
|
21
|
+
from secenvY.services.reward_calculator import RewardCalculator
|
|
22
|
+
from secenvY.services.state_builder import StateBuilder
|
|
23
|
+
from secenvY.services.state_encoder import StateEncoder
|
|
24
|
+
from secenvY.services.state_validator import StateValidator
|
|
25
|
+
from secenvY.services.tensor_converter import TensorConverter
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"SecurityEnv",
|
|
29
|
+
"DuelingDQNAgent",
|
|
30
|
+
"DuelingQNetwork",
|
|
31
|
+
"PrioritizedReplayBuffer",
|
|
32
|
+
"UniformReplayBuffer",
|
|
33
|
+
"SystemGateway",
|
|
34
|
+
"MockSystemGateway",
|
|
35
|
+
"WebSocketSystemGateway",
|
|
36
|
+
"ActionMapper",
|
|
37
|
+
"StateBuilder",
|
|
38
|
+
"StateEncoder",
|
|
39
|
+
"StateValidator",
|
|
40
|
+
"TensorConverter",
|
|
41
|
+
"RewardCalculator",
|
|
42
|
+
"EpisodePolicy",
|
|
43
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Dueling Double Deep Q-Network agent, neural network architectures, and replay buffers."""
|
|
2
|
+
|
|
3
|
+
from secenvY.agents.dqn_agent import DuelingDQNAgent
|
|
4
|
+
from secenvY.agents.networks import DuelingQNetwork
|
|
5
|
+
from secenvY.agents.replay_buffer import (
|
|
6
|
+
PrioritizedReplayBuffer,
|
|
7
|
+
SumTree,
|
|
8
|
+
UniformReplayBuffer,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DuelingDQNAgent",
|
|
13
|
+
"DuelingQNetwork",
|
|
14
|
+
"PrioritizedReplayBuffer",
|
|
15
|
+
"UniformReplayBuffer",
|
|
16
|
+
"SumTree",
|
|
17
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Dueling Double Deep Q-Network (Dueling Double DQN) Agent with Prioritized Experience Replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
import torch.nn as nn
|
|
11
|
+
import torch.optim as optim
|
|
12
|
+
|
|
13
|
+
from secenvY.agents.networks import DuelingQNetwork
|
|
14
|
+
from secenvY.agents.replay_buffer import PrioritizedReplayBuffer, UniformReplayBuffer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DuelingDQNAgent:
|
|
18
|
+
"""Reinforcement Learning Agent implementing Dueling Double DQN with Prioritized Experience Replay (PER).
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
state_dim: Dimension of observation tensor (default: 117).
|
|
22
|
+
action_dim: Number of discrete actions (default: 8).
|
|
23
|
+
hidden_dim: Hidden dimension of backbone network (default: 256).
|
|
24
|
+
lr: Learning rate for Adam optimizer (default: 1e-4).
|
|
25
|
+
gamma: Discount factor for Bellman target (default: 0.99).
|
|
26
|
+
tau: Soft target network update coefficient (default: 0.005).
|
|
27
|
+
target_update_interval: Hard update interval if tau is 1.0 (default: 500).
|
|
28
|
+
buffer_capacity: Max transitions stored in experience replay (default: 100,000).
|
|
29
|
+
use_per: Whether to use Prioritized Experience Replay (default: True).
|
|
30
|
+
per_alpha: PER prioritization exponent (default: 0.6).
|
|
31
|
+
per_beta_start: Initial importance sampling exponent (default: 0.4).
|
|
32
|
+
max_grad_norm: Maximum gradient norm for clipping (default: 10.0).
|
|
33
|
+
device: Torch computing device (default: cpu / cuda if available).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
state_dim: int = 117,
|
|
39
|
+
action_dim: int = 8,
|
|
40
|
+
hidden_dim: int = 256,
|
|
41
|
+
lr: float = 1e-4,
|
|
42
|
+
gamma: float = 0.99,
|
|
43
|
+
tau: float = 0.005,
|
|
44
|
+
target_update_interval: int = 500,
|
|
45
|
+
buffer_capacity: int = 100_000,
|
|
46
|
+
use_per: bool = True,
|
|
47
|
+
per_alpha: float = 0.6,
|
|
48
|
+
per_beta_start: float = 0.4,
|
|
49
|
+
max_grad_norm: float = 10.0,
|
|
50
|
+
device: Optional[Union[str, torch.device]] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if device is None:
|
|
53
|
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
54
|
+
else:
|
|
55
|
+
self.device = torch.device(device)
|
|
56
|
+
|
|
57
|
+
self.state_dim = state_dim
|
|
58
|
+
self.action_dim = action_dim
|
|
59
|
+
self.gamma = gamma
|
|
60
|
+
self.tau = tau
|
|
61
|
+
self.target_update_interval = target_update_interval
|
|
62
|
+
self.max_grad_norm = max_grad_norm
|
|
63
|
+
self.use_per = use_per
|
|
64
|
+
|
|
65
|
+
# Neural Networks: Online and Target Q-Networks
|
|
66
|
+
self.online_net = DuelingQNetwork(state_dim, action_dim, hidden_dim).to(self.device)
|
|
67
|
+
self.target_net = DuelingQNetwork(state_dim, action_dim, hidden_dim).to(self.device)
|
|
68
|
+
self.target_net.load_state_dict(self.online_net.state_dict())
|
|
69
|
+
self.target_net.eval()
|
|
70
|
+
for param in self.target_net.parameters():
|
|
71
|
+
param.requires_grad = False
|
|
72
|
+
|
|
73
|
+
# Optimizer and Huber Loss Criterion
|
|
74
|
+
self.optimizer = optim.Adam(self.online_net.parameters(), lr=lr, eps=1e-8)
|
|
75
|
+
self.criterion = nn.SmoothL1Loss(reduction="none")
|
|
76
|
+
|
|
77
|
+
# Replay Memory (PER or Uniform)
|
|
78
|
+
self.replay_buffer: Union[PrioritizedReplayBuffer, UniformReplayBuffer]
|
|
79
|
+
if use_per:
|
|
80
|
+
self.replay_buffer = PrioritizedReplayBuffer(
|
|
81
|
+
capacity=buffer_capacity,
|
|
82
|
+
alpha=per_alpha,
|
|
83
|
+
beta_start=per_beta_start,
|
|
84
|
+
device=self.device,
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
self.replay_buffer = UniformReplayBuffer(
|
|
88
|
+
capacity=buffer_capacity,
|
|
89
|
+
device=self.device,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.total_steps = 0
|
|
93
|
+
self.train_steps = 0
|
|
94
|
+
|
|
95
|
+
def select_action(
|
|
96
|
+
self,
|
|
97
|
+
state: np.ndarray | List[float],
|
|
98
|
+
epsilon: float = 0.0,
|
|
99
|
+
action_mask: Optional[Union[np.ndarray, List[bool]]] = None,
|
|
100
|
+
) -> int:
|
|
101
|
+
"""Select action using epsilon-greedy exploration policy with optional action masking.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
state: State vector of shape (117,) or list of floats.
|
|
105
|
+
epsilon: Exploration probability in [0.0, 1.0].
|
|
106
|
+
action_mask: Optional boolean mask of shape (action_dim,) where True indicates valid action.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Discrete action ID in range [0, action_dim - 1].
|
|
110
|
+
"""
|
|
111
|
+
self.total_steps += 1
|
|
112
|
+
|
|
113
|
+
if action_mask is not None:
|
|
114
|
+
mask_arr = np.array(action_mask, dtype=bool)
|
|
115
|
+
valid_indices = np.where(mask_arr)[0]
|
|
116
|
+
if len(valid_indices) == 0:
|
|
117
|
+
valid_indices = np.arange(self.action_dim)
|
|
118
|
+
else:
|
|
119
|
+
valid_indices = np.arange(self.action_dim)
|
|
120
|
+
|
|
121
|
+
if random.random() < epsilon:
|
|
122
|
+
return int(random.choice(valid_indices))
|
|
123
|
+
|
|
124
|
+
state_tensor = torch.tensor(
|
|
125
|
+
np.array(state, dtype=np.float32),
|
|
126
|
+
device=self.device,
|
|
127
|
+
dtype=torch.float32,
|
|
128
|
+
).unsqueeze(0)
|
|
129
|
+
|
|
130
|
+
self.online_net.eval()
|
|
131
|
+
with torch.no_grad():
|
|
132
|
+
q_values = self.online_net(state_tensor).squeeze(0)
|
|
133
|
+
if action_mask is not None:
|
|
134
|
+
mask_tensor = torch.tensor(mask_arr, device=self.device, dtype=torch.bool)
|
|
135
|
+
q_values[~mask_tensor] = -1e9
|
|
136
|
+
action = int(q_values.argmax(dim=-1).item())
|
|
137
|
+
self.online_net.train()
|
|
138
|
+
|
|
139
|
+
return action
|
|
140
|
+
|
|
141
|
+
def train_step(self, batch_size: int = 32) -> Optional[float]:
|
|
142
|
+
"""Perform a single Double DQN training step over a batch from replay buffer.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
batch_size: Number of transitions to sample.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Computed scalar loss value, or None if buffer has insufficient transitions.
|
|
149
|
+
"""
|
|
150
|
+
if len(self.replay_buffer) < batch_size:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
self.train_steps += 1
|
|
154
|
+
|
|
155
|
+
# 1. Sample batch
|
|
156
|
+
if self.use_per:
|
|
157
|
+
states, actions, rewards, next_states, dones, indices, weights = self.replay_buffer.sample(batch_size) # type: ignore
|
|
158
|
+
else:
|
|
159
|
+
states, actions, rewards, next_states, dones = self.replay_buffer.sample(batch_size) # type: ignore
|
|
160
|
+
weights = torch.ones_like(rewards)
|
|
161
|
+
indices = []
|
|
162
|
+
|
|
163
|
+
# 2. Compute current Q-values Q(s, a; θ)
|
|
164
|
+
current_q = self.online_net(states).gather(1, actions)
|
|
165
|
+
|
|
166
|
+
# 3. Double DQN Target: a* = argmax_{a'} Q(s', a'; θ), y = r + γ Q(s', a*; θ^-)
|
|
167
|
+
with torch.no_grad():
|
|
168
|
+
next_actions = self.online_net(next_states).argmax(dim=-1, keepdim=True)
|
|
169
|
+
target_q_next = self.target_net(next_states).gather(1, next_actions)
|
|
170
|
+
target_q = rewards + (1.0 - dones) * self.gamma * target_q_next
|
|
171
|
+
|
|
172
|
+
# 4. Compute TD Errors and Huber Loss
|
|
173
|
+
td_errors = (current_q - target_q).detach()
|
|
174
|
+
loss_elements = self.criterion(current_q, target_q)
|
|
175
|
+
loss = (loss_elements * weights).mean()
|
|
176
|
+
|
|
177
|
+
# 5. Backpropagation and Gradient Clipping
|
|
178
|
+
self.optimizer.zero_grad()
|
|
179
|
+
loss.backward()
|
|
180
|
+
if self.max_grad_norm > 0:
|
|
181
|
+
nn.utils.clip_grad_norm_(self.online_net.parameters(), max_norm=self.max_grad_norm)
|
|
182
|
+
self.optimizer.step()
|
|
183
|
+
|
|
184
|
+
# 6. Update PER priorities
|
|
185
|
+
if self.use_per and indices:
|
|
186
|
+
self.replay_buffer.update_priorities(indices, td_errors.abs()) # type: ignore
|
|
187
|
+
|
|
188
|
+
# 7. Update Target Network
|
|
189
|
+
self._update_target_network()
|
|
190
|
+
|
|
191
|
+
return float(loss.item())
|
|
192
|
+
|
|
193
|
+
def _update_target_network(self) -> None:
|
|
194
|
+
"""Update target network parameters via Polyak soft update or periodic hard copy."""
|
|
195
|
+
if self.tau < 1.0:
|
|
196
|
+
# Soft Polyak update: θ^- ← τ θ + (1 - τ) θ^-
|
|
197
|
+
for target_param, online_param in zip(self.target_net.parameters(), self.online_net.parameters()):
|
|
198
|
+
target_param.data.copy_(self.tau * online_param.data + (1.0 - self.tau) * target_param.data)
|
|
199
|
+
else:
|
|
200
|
+
# Hard periodic update
|
|
201
|
+
if self.train_steps % self.target_update_interval == 0:
|
|
202
|
+
self.target_net.load_state_dict(self.online_net.state_dict())
|
|
203
|
+
|
|
204
|
+
def save_checkpoint(self, filepath: str) -> None:
|
|
205
|
+
"""Save model and optimizer state to disk."""
|
|
206
|
+
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
|
|
207
|
+
torch.save(
|
|
208
|
+
{
|
|
209
|
+
"online_net_state": self.online_net.state_dict(),
|
|
210
|
+
"target_net_state": self.target_net.state_dict(),
|
|
211
|
+
"optimizer_state": self.optimizer.state_dict(),
|
|
212
|
+
"total_steps": self.total_steps,
|
|
213
|
+
"train_steps": self.train_steps,
|
|
214
|
+
},
|
|
215
|
+
filepath,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def load_checkpoint(self, filepath: str) -> None:
|
|
219
|
+
"""Load model and optimizer state from disk."""
|
|
220
|
+
checkpoint = torch.load(filepath, map_location=self.device)
|
|
221
|
+
self.online_net.load_state_dict(checkpoint["online_net_state"])
|
|
222
|
+
self.target_net.load_state_dict(checkpoint["target_net_state"])
|
|
223
|
+
self.optimizer.load_state_dict(checkpoint["optimizer_state"])
|
|
224
|
+
self.total_steps = checkpoint.get("total_steps", 0)
|
|
225
|
+
self.train_steps = checkpoint.get("train_steps", 0)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""PyTorch neural network architectures for Dueling Deep Q-Networks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
import torch.nn as nn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuelingQNetwork(nn.Module):
|
|
10
|
+
"""Dueling Deep Q-Network decomposing Q(s, a) into Value V(s) and Advantage A(s, a) streams.
|
|
11
|
+
|
|
12
|
+
Q(s, a) = V(s) + (A(s, a) - 1/|A| * sum_{a'} A(s, a'))
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
state_dim: Input observation space dimension (default: 117).
|
|
16
|
+
action_dim: Discrete action space size (default: 8).
|
|
17
|
+
hidden_dim: Number of hidden units in backbone layers (default: 256).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
state_dim: int = 117,
|
|
23
|
+
action_dim: int = 8,
|
|
24
|
+
hidden_dim: int = 256,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.state_dim = state_dim
|
|
28
|
+
self.action_dim = action_dim
|
|
29
|
+
|
|
30
|
+
# Shared Feature Extractor Backbone
|
|
31
|
+
self.backbone = nn.Sequential(
|
|
32
|
+
nn.Linear(state_dim, hidden_dim),
|
|
33
|
+
nn.LayerNorm(hidden_dim),
|
|
34
|
+
nn.ReLU(),
|
|
35
|
+
nn.Linear(hidden_dim, hidden_dim),
|
|
36
|
+
nn.ReLU(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Value Stream: V(s) -> [B, 1]
|
|
40
|
+
self.value_stream = nn.Sequential(
|
|
41
|
+
nn.Linear(hidden_dim, hidden_dim // 2),
|
|
42
|
+
nn.ReLU(),
|
|
43
|
+
nn.Linear(hidden_dim // 2, 1),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Advantage Stream: A(s, a) -> [B, action_dim]
|
|
47
|
+
self.advantage_stream = nn.Sequential(
|
|
48
|
+
nn.Linear(hidden_dim, hidden_dim // 2),
|
|
49
|
+
nn.ReLU(),
|
|
50
|
+
nn.Linear(hidden_dim // 2, action_dim),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def forward(self, state: torch.Tensor) -> torch.Tensor:
|
|
54
|
+
"""Forward pass computing Q-values for all discrete actions.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
state: Tensor of shape (batch_size, state_dim) or (state_dim,).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Tensor of shape (batch_size, action_dim) containing Q-values.
|
|
61
|
+
"""
|
|
62
|
+
if state.dim() == 1:
|
|
63
|
+
state = state.unsqueeze(0)
|
|
64
|
+
|
|
65
|
+
features = self.backbone(state)
|
|
66
|
+
value = self.value_stream(features) # [B, 1]
|
|
67
|
+
advantage = self.advantage_stream(features) # [B, action_dim]
|
|
68
|
+
|
|
69
|
+
# Aggregation: Q(s, a) = V(s) + (A(s, a) - mean(A(s, :)))
|
|
70
|
+
q_values = value + (advantage - advantage.mean(dim=-1, keepdim=True))
|
|
71
|
+
return q_values
|
|
72
|
+
|
|
73
|
+
def compute_value_and_advantage(self, state: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
74
|
+
"""Expose raw value and advantage streams for inspection and explainability."""
|
|
75
|
+
if state.dim() == 1:
|
|
76
|
+
state = state.unsqueeze(0)
|
|
77
|
+
|
|
78
|
+
features = self.backbone(state)
|
|
79
|
+
value = self.value_stream(features)
|
|
80
|
+
advantage = self.advantage_stream(features)
|
|
81
|
+
return value, advantage
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Uniform and Prioritized Experience Replay buffers for RL agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
import random
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UniformReplayBuffer:
|
|
13
|
+
"""Standard circular Experience Replay buffer sampling uniformly at random."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, capacity: int = 100_000, device: Optional[torch.device] = None) -> None:
|
|
16
|
+
self.capacity = capacity
|
|
17
|
+
self.device = device or torch.device("cpu")
|
|
18
|
+
self.buffer: deque = deque(maxlen=capacity)
|
|
19
|
+
|
|
20
|
+
def push(
|
|
21
|
+
self,
|
|
22
|
+
state: np.ndarray | List[float],
|
|
23
|
+
action: int,
|
|
24
|
+
reward: float,
|
|
25
|
+
next_state: np.ndarray | List[float],
|
|
26
|
+
done: bool,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Add a transition tuple to the buffer."""
|
|
29
|
+
self.buffer.append((
|
|
30
|
+
np.array(state, dtype=np.float32),
|
|
31
|
+
int(action),
|
|
32
|
+
float(reward),
|
|
33
|
+
np.array(next_state, dtype=np.float32),
|
|
34
|
+
bool(done),
|
|
35
|
+
))
|
|
36
|
+
|
|
37
|
+
def sample(self, batch_size: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
38
|
+
"""Sample a batch of transitions uniformly at random."""
|
|
39
|
+
transitions = random.sample(self.buffer, batch_size)
|
|
40
|
+
states, actions, rewards, next_states, dones = zip(*transitions)
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
torch.tensor(np.array(states), dtype=torch.float32, device=self.device),
|
|
44
|
+
torch.tensor(actions, dtype=torch.long, device=self.device).unsqueeze(1),
|
|
45
|
+
torch.tensor(rewards, dtype=torch.float32, device=self.device).unsqueeze(1),
|
|
46
|
+
torch.tensor(np.array(next_states), dtype=torch.float32, device=self.device),
|
|
47
|
+
torch.tensor(dones, dtype=torch.float32, device=self.device).unsqueeze(1),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __len__(self) -> int:
|
|
51
|
+
return len(self.buffer)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SumTree:
|
|
55
|
+
"""Binary SumTree data structure for O(log N) prioritized sampling and priority updates."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, capacity: int) -> None:
|
|
58
|
+
self.capacity = capacity
|
|
59
|
+
self.tree = np.zeros(2 * capacity - 1, dtype=np.float64)
|
|
60
|
+
self.data: List[Optional[Tuple[np.ndarray, int, float, np.ndarray, bool]]] = [None] * capacity
|
|
61
|
+
self.write_ptr = 0
|
|
62
|
+
self.n_entries = 0
|
|
63
|
+
|
|
64
|
+
def _propagate(self, idx: int, change: float) -> None:
|
|
65
|
+
parent = (idx - 1) // 2
|
|
66
|
+
self.tree[parent] += change
|
|
67
|
+
if parent != 0:
|
|
68
|
+
self._propagate(parent, change)
|
|
69
|
+
|
|
70
|
+
def _retrieve(self, idx: int, s: float) -> int:
|
|
71
|
+
left = 2 * idx + 1
|
|
72
|
+
right = left + 1
|
|
73
|
+
|
|
74
|
+
if left >= len(self.tree):
|
|
75
|
+
return idx
|
|
76
|
+
|
|
77
|
+
if s <= self.tree[left]:
|
|
78
|
+
return self._retrieve(left, s)
|
|
79
|
+
else:
|
|
80
|
+
return self._retrieve(right, s - self.tree[left])
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def total_priority(self) -> float:
|
|
84
|
+
return float(self.tree[0])
|
|
85
|
+
|
|
86
|
+
def add(self, priority: float, data: Tuple[np.ndarray, int, float, np.ndarray, bool]) -> None:
|
|
87
|
+
tree_idx = self.write_ptr + self.capacity - 1
|
|
88
|
+
self.data[self.write_ptr] = data
|
|
89
|
+
self.update(tree_idx, priority)
|
|
90
|
+
|
|
91
|
+
self.write_ptr = (self.write_ptr + 1) % self.capacity
|
|
92
|
+
if self.n_entries < self.capacity:
|
|
93
|
+
self.n_entries += 1
|
|
94
|
+
|
|
95
|
+
def update(self, tree_idx: int, priority: float) -> None:
|
|
96
|
+
change = priority - self.tree[tree_idx]
|
|
97
|
+
self.tree[tree_idx] = priority
|
|
98
|
+
if tree_idx != 0:
|
|
99
|
+
self._propagate(tree_idx, change)
|
|
100
|
+
|
|
101
|
+
def get_leaf(self, val: float) -> Tuple[int, float, Tuple[np.ndarray, int, float, np.ndarray, bool]]:
|
|
102
|
+
tree_idx = self._retrieve(0, val)
|
|
103
|
+
data_idx = tree_idx - self.capacity + 1
|
|
104
|
+
return tree_idx, float(self.tree[tree_idx]), self.data[data_idx] # type: ignore
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PrioritizedReplayBuffer:
|
|
108
|
+
"""Prioritized Experience Replay (PER) buffer using SumTree for efficient sampling."""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
capacity: int = 100_000,
|
|
113
|
+
alpha: float = 0.6,
|
|
114
|
+
beta_start: float = 0.4,
|
|
115
|
+
beta_frames: int = 100_000,
|
|
116
|
+
eps: float = 1e-5,
|
|
117
|
+
device: Optional[torch.device] = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
self.tree = SumTree(capacity)
|
|
120
|
+
self.capacity = capacity
|
|
121
|
+
self.alpha = alpha
|
|
122
|
+
self.beta_start = beta_start
|
|
123
|
+
self.beta_frames = beta_frames
|
|
124
|
+
self.eps = eps
|
|
125
|
+
self.device = device or torch.device("cpu")
|
|
126
|
+
self.max_priority = 1.0
|
|
127
|
+
self.frame = 0
|
|
128
|
+
|
|
129
|
+
def _get_beta(self) -> float:
|
|
130
|
+
return min(1.0, self.beta_start + self.frame * (1.0 - self.beta_start) / max(1, self.beta_frames))
|
|
131
|
+
|
|
132
|
+
def push(
|
|
133
|
+
self,
|
|
134
|
+
state: np.ndarray | List[float],
|
|
135
|
+
action: int,
|
|
136
|
+
reward: float,
|
|
137
|
+
next_state: np.ndarray | List[float],
|
|
138
|
+
done: bool,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Add transition with maximal priority to ensure initial exploration."""
|
|
141
|
+
data = (
|
|
142
|
+
np.array(state, dtype=np.float32),
|
|
143
|
+
int(action),
|
|
144
|
+
float(reward),
|
|
145
|
+
np.array(next_state, dtype=np.float32),
|
|
146
|
+
bool(done),
|
|
147
|
+
)
|
|
148
|
+
priority = (self.max_priority + self.eps) ** self.alpha
|
|
149
|
+
self.tree.add(priority, data)
|
|
150
|
+
|
|
151
|
+
def sample(
|
|
152
|
+
self, batch_size: int
|
|
153
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, List[int], torch.Tensor]:
|
|
154
|
+
"""Sample a batch proportionally to priorities with Importance Sampling weights."""
|
|
155
|
+
self.frame += 1
|
|
156
|
+
beta = self._get_beta()
|
|
157
|
+
|
|
158
|
+
segment = self.tree.total_priority / batch_size
|
|
159
|
+
indices: List[int] = []
|
|
160
|
+
priorities: List[float] = []
|
|
161
|
+
transitions: List[Tuple[np.ndarray, int, float, np.ndarray, bool]] = []
|
|
162
|
+
|
|
163
|
+
for i in range(batch_size):
|
|
164
|
+
a = segment * i
|
|
165
|
+
b = segment * (i + 1)
|
|
166
|
+
s = random.uniform(a, b)
|
|
167
|
+
tree_idx, p, data = self.tree.get_leaf(s)
|
|
168
|
+
|
|
169
|
+
if data is None:
|
|
170
|
+
# Fallback for floating point boundary
|
|
171
|
+
tree_idx, p, data = self.tree.get_leaf(random.uniform(0, max(1e-5, self.tree.total_priority)))
|
|
172
|
+
|
|
173
|
+
indices.append(tree_idx)
|
|
174
|
+
priorities.append(max(self.eps, p))
|
|
175
|
+
transitions.append(data)
|
|
176
|
+
|
|
177
|
+
# Importance sampling weights: w_i = (N * P(i))^(-beta) / max(w)
|
|
178
|
+
probs = np.array(priorities) / max(1e-8, self.tree.total_priority)
|
|
179
|
+
weights = (len(self) * probs) ** (-beta)
|
|
180
|
+
weights = weights / weights.max()
|
|
181
|
+
|
|
182
|
+
states, actions, rewards, next_states, dones = zip(*transitions)
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
torch.tensor(np.array(states), dtype=torch.float32, device=self.device),
|
|
186
|
+
torch.tensor(actions, dtype=torch.long, device=self.device).unsqueeze(1),
|
|
187
|
+
torch.tensor(rewards, dtype=torch.float32, device=self.device).unsqueeze(1),
|
|
188
|
+
torch.tensor(np.array(next_states), dtype=torch.float32, device=self.device),
|
|
189
|
+
torch.tensor(dones, dtype=torch.float32, device=self.device).unsqueeze(1),
|
|
190
|
+
indices,
|
|
191
|
+
torch.tensor(weights, dtype=torch.float32, device=self.device).unsqueeze(1),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def update_priorities(self, indices: List[int], td_errors: np.ndarray | torch.Tensor) -> None:
|
|
195
|
+
"""Update transition priorities with new TD errors."""
|
|
196
|
+
if isinstance(td_errors, torch.Tensor):
|
|
197
|
+
td_errors = td_errors.detach().cpu().numpy().flatten()
|
|
198
|
+
|
|
199
|
+
for idx, error in zip(indices, td_errors):
|
|
200
|
+
p = float((abs(error) + self.eps) ** self.alpha)
|
|
201
|
+
self.tree.update(idx, p)
|
|
202
|
+
self.max_priority = max(self.max_priority, abs(error))
|
|
203
|
+
|
|
204
|
+
def __len__(self) -> int:
|
|
205
|
+
return self.tree.n_entries
|
secenvY/core/__init__.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Core schemas, vocabularies, and exception definitions for secenvY."""
|
|
2
|
+
|
|
3
|
+
from secenvY.core.exceptions import (
|
|
4
|
+
EpisodeInactiveError,
|
|
5
|
+
GatewayError,
|
|
6
|
+
InvalidActionError,
|
|
7
|
+
SecurityEnvError,
|
|
8
|
+
SessionError,
|
|
9
|
+
StateValidationError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from secenvY.core.schemas import (
|
|
13
|
+
ActionCommand,
|
|
14
|
+
AndroidState,
|
|
15
|
+
AppMetadataState,
|
|
16
|
+
DynamicCryptoRuntimeState,
|
|
17
|
+
DynamicExportedIPCState,
|
|
18
|
+
DynamicLoggingState,
|
|
19
|
+
DynamicNetworkState,
|
|
20
|
+
DynamicStorageState,
|
|
21
|
+
DynamicUIState,
|
|
22
|
+
DynamicWebViewRuntimeState,
|
|
23
|
+
ExecutionResult,
|
|
24
|
+
ExplorationCoverageState,
|
|
25
|
+
MetaAction,
|
|
26
|
+
PermissionState,
|
|
27
|
+
RAGSecurityEvaluation,
|
|
28
|
+
RuntimeLifecycleState,
|
|
29
|
+
SecurityAnalysis,
|
|
30
|
+
Severity,
|
|
31
|
+
StaticAttackSurfaceState,
|
|
32
|
+
StaticCallGraphState,
|
|
33
|
+
StaticCryptoState,
|
|
34
|
+
StaticStorageState,
|
|
35
|
+
StaticWebViewState,
|
|
36
|
+
TransitionRecord,
|
|
37
|
+
VulnerabilityFlags,
|
|
38
|
+
)
|
|
39
|
+
from secenvY.core.vocabularies import (
|
|
40
|
+
ACTION_CATEGORIES,
|
|
41
|
+
ACTION_CATEGORIES_LIST,
|
|
42
|
+
ACTION_CATEGORIES_MAP,
|
|
43
|
+
CRITICAL_PERMISSIONS,
|
|
44
|
+
CRITICAL_PERMISSIONS_LIST,
|
|
45
|
+
CRITICAL_PERMISSIONS_MAP,
|
|
46
|
+
CRYPTO_ALGORITHMS_LIST,
|
|
47
|
+
CRYPTO_ALGORITHMS_MAP,
|
|
48
|
+
STATIC_CRYPTO_ALGORITHMS,
|
|
49
|
+
ActionCategory,
|
|
50
|
+
CriticalPermission,
|
|
51
|
+
CryptoAlgorithm,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# Exceptions
|
|
56
|
+
"SecurityEnvError",
|
|
57
|
+
"StateValidationError",
|
|
58
|
+
"ValidationError",
|
|
59
|
+
"GatewayError",
|
|
60
|
+
"InvalidActionError",
|
|
61
|
+
"SessionError",
|
|
62
|
+
"EpisodeInactiveError",
|
|
63
|
+
# Vocabularies & Enums
|
|
64
|
+
"ActionCategory",
|
|
65
|
+
"MetaAction",
|
|
66
|
+
"CriticalPermission",
|
|
67
|
+
"CryptoAlgorithm",
|
|
68
|
+
"CRITICAL_PERMISSIONS",
|
|
69
|
+
"CRITICAL_PERMISSIONS_LIST",
|
|
70
|
+
"CRITICAL_PERMISSIONS_MAP",
|
|
71
|
+
"STATIC_CRYPTO_ALGORITHMS",
|
|
72
|
+
"CRYPTO_ALGORITHMS_LIST",
|
|
73
|
+
"CRYPTO_ALGORITHMS_MAP",
|
|
74
|
+
"ACTION_CATEGORIES",
|
|
75
|
+
"ACTION_CATEGORIES_LIST",
|
|
76
|
+
"ACTION_CATEGORIES_MAP",
|
|
77
|
+
# Canonical State Models
|
|
78
|
+
"AndroidState",
|
|
79
|
+
"AppMetadataState",
|
|
80
|
+
"PermissionState",
|
|
81
|
+
"StaticAttackSurfaceState",
|
|
82
|
+
"StaticStorageState",
|
|
83
|
+
"StaticCryptoState",
|
|
84
|
+
"StaticWebViewState",
|
|
85
|
+
"StaticCallGraphState",
|
|
86
|
+
"RuntimeLifecycleState",
|
|
87
|
+
"DynamicUIState",
|
|
88
|
+
"DynamicLoggingState",
|
|
89
|
+
"DynamicStorageState",
|
|
90
|
+
"DynamicCryptoRuntimeState",
|
|
91
|
+
"DynamicWebViewRuntimeState",
|
|
92
|
+
"DynamicNetworkState",
|
|
93
|
+
"DynamicExportedIPCState",
|
|
94
|
+
"ExplorationCoverageState",
|
|
95
|
+
"VulnerabilityFlags",
|
|
96
|
+
# Commands & Results
|
|
97
|
+
"ActionCommand",
|
|
98
|
+
"ExecutionResult",
|
|
99
|
+
"TransitionRecord",
|
|
100
|
+
# RAG Reasoner Schemas
|
|
101
|
+
"Severity",
|
|
102
|
+
"RAGSecurityEvaluation",
|
|
103
|
+
"SecurityAnalysis",
|
|
104
|
+
# Transitional Aliases
|
|
105
|
+
"ValidatedObservation",
|
|
106
|
+
"StaticAnalysisContext",
|
|
107
|
+
"UIContext",
|
|
108
|
+
"FridaRuntimeSinks",
|
|
109
|
+
]
|