Skip to content

Commit 7856f9b

Browse files
jaggerclaude
andcommitted
Add global and per-user action/category enable/disable controls (v0.5.0)
Global config: - DisabledActions: comma-separated list of action names to skip globally - DisabledCategories: comma-separated categories (Core, Management, Advanced) - Select-ROUserActions filters out globally disabled actions/categories - Set-ROConfig now accepts empty strings (AllowEmptyString) for clearing Per-user controls via Set-ROUser: - -DisableCategory / -EnableCategory: set all actions in a category to 0 or restore default weights from SeedActionWeights.psd1 - -DisableAction / -EnableAction: toggle individual actions by name Global disables take precedence over per-user weights. Bump module version to 0.5.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9fd1144 commit 7856f9b

File tree

9 files changed

+260
-6
lines changed

9 files changed

+260
-6
lines changed

CLAUDE.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# RobOtters -- Project Conventions
2+
3+
## Overview
4+
PowerShell module (RobOtters) that simulates Secret Server user activity for lab environments.
5+
AD-authenticated users perform randomized actions (0-15 per 30-min cycle) against
6+
an on-prem Delinea Secret Server instance to generate realistic audit trail data.
7+
8+
## Architecture
9+
- **PowerShell module** (`RobOtters.psd1` / `RobOtters.psm1`) with Public/Private function split
10+
- **SQLite** via PSSQLite module for credential store, config, and action logs
11+
- **REST API** calls to Secret Server `/api/v1/*` endpoints with OAuth2 password grant
12+
- Runs unattended via **Windows Task Scheduler** every 30 minutes
13+
14+
## Directory Layout
15+
```
16+
RobOtters/
17+
+-- RobOtters.psd1 # Module manifest
18+
+-- RobOtters.psm1 # Dot-source loader
19+
+-- Register-ROTask.ps1 # Task Scheduler registration
20+
+-- assets/ # Images
21+
+-- Data/ # Schema, seed data, SS reports
22+
+-- Docs/ # Guides and command reference
23+
+-- Public/ # Exported cmdlets (13 commands)
24+
+-- Private/ # Internal functions
25+
| +-- Data/ # DB helpers
26+
| +-- Api/ # Secret Server REST client
27+
| +-- Actions/ # 19 Secret Server action functions
28+
| +-- Engine/ # Cycle orchestration
29+
| +-- Logging/ # File + DB logging
30+
+-- Scripts/ # Migration utilities
31+
+-- Tests/ # Pester tests
32+
```
33+
34+
## Data Storage
35+
- **SQLite DB** lives in `$env:ProgramData\RobOtters\` (or `$env:RO_DATA_PATH` if set)
36+
- **Log files** in a `Logs/` subfolder under the data root
37+
- DB and logs are outside the repo directory; gitignored
38+
39+
## Coding Conventions
40+
- **Verb-Noun** naming: all functions use `RO` prefix (`Verb-RO<Noun>`)
41+
- All functions use `[CmdletBinding()]` and named parameters
42+
- Action functions return uniform `[PSCustomObject]@{ Action; TargetType; TargetId; TargetName; Success; ErrorMessage }`
43+
- Use `Write-ROLog` for all operational logging (not Write-Host)
44+
- Errors: use `Write-Error` / `throw` for unrecoverable; `Write-Warning` + continue for transient
45+
- SQL: always parameterized queries via `-SqlParameters` (no string interpolation)
46+
- Secrets: passwords encrypted at rest (DPAPI by default, AES-256 if `RO_ENCRYPT_KEY` env var is set)
47+
- No aliases in scripts; use full cmdlet names
48+
- Prefer splatting for calls with 3+ parameters
49+
50+
## Key Dependencies
51+
- **PSSQLite** -- SQLite access (`Invoke-SqliteQuery`)
52+
- **Secret Server REST API** -- `/api/v1/*` with OAuth2 bearer tokens
53+
54+
## Config Defaults (stored in Config table)
55+
- `SecretServerUrl` -- base URL of the SS instance
56+
- `MinActionsPerCycle` -- 0
57+
- `MaxActionsPerCycle` -- 15
58+
- `LogRetentionDays` -- 30
59+
- `DefaultDomain` -- lab domain name
60+
- `PasswordRotationDays` -- 14 (days between automatic password rotations)
61+
- `AuthFailureAction` -- AlertOnly (or RotateAndAlert)
62+
- `LauncherTemplateId` -- template ID for launcher-based actions
63+
- `AccessSnapshotMaxAgeDays` -- max age for user access snapshots
64+
- `DisabledActions` -- comma-separated list of globally disabled action names
65+
- `DisabledCategories` -- comma-separated list of globally disabled categories (Core, Management, Advanced)
66+
67+
## Testing
68+
- Pester v5+ for unit tests
69+
- `Tests/Unit/` -- pure logic tests (no network/DB)
70+
- `Tests/Integration/` -- requires live SS instance

Docs/commands/Set-ROUser.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ then updated (DPAPI-encrypted) in the SQLite database. The -Password and
3232
| IsEnabled | Nullable[Boolean] | No | -- | Enable or disable the user |
3333
| ActionWeights | Hashtable | No | -- | Updated action weight map |
3434
| RandomPassword | Switch | No | -- | Generate and set a random password (mutually exclusive with Password) |
35+
| EnableCategory | String | No | -- | Enable all actions in a category (Core, Management, Advanced) by restoring default weights |
36+
| DisableCategory | String | No | -- | Disable all actions in a category by setting weights to 0 |
37+
| EnableAction | String | No | -- | Enable a specific action by restoring its default weight |
38+
| DisableAction | String | No | -- | Disable a specific action by setting its weight to 0 |
3539

3640
## Examples
3741

@@ -59,6 +63,23 @@ Set-ROUser -Username 'svc-simuser01' -ActionWeights @{ GetSecret = 10; CreateSec
5963
```
6064
Adjusts the probability weights for the user's actions during simulation cycles.
6165

66+
### Example 5: Disable all Management actions for a user
67+
```powershell
68+
Set-ROUser -Username 'svc-simuser01' -DisableCategory 'Management'
69+
```
70+
Sets all Management action weights to 0.
71+
72+
### Example 6: Re-enable a category
73+
```powershell
74+
Set-ROUser -Username 'svc-simuser01' -EnableCategory 'Management'
75+
```
76+
Restores default weights for all Management actions.
77+
78+
### Example 7: Disable a single action
79+
```powershell
80+
Set-ROUser -Username 'svc-simuser01' -DisableAction 'CreateSecret'
81+
```
82+
6283
## Outputs
6384

6485
| Property | Type | Description |

Docs/configuration.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Config Keys
44

5-
All configuration is stored in the Config SQLite table. Use Get-ROConfig and Set-ROConfig to read and write values.
5+
All configuration is stored in the Config SQLite table (11 keys). Use Get-ROConfig and Set-ROConfig to read and write values.
66

77
| Key | Default | Description |
88
|-----|---------|-------------|
@@ -15,6 +15,8 @@ All configuration is stored in the Config SQLite table. Use Get-ROConfig and Set
1515
| AuthFailureAction | AlertOnly | Auth failure behavior: AlertOnly or RotateAndAlert |
1616
| LauncherTemplateId | 6052 | Template ID for launcher-based secret actions |
1717
| AccessSnapshotMaxAgeDays | 7 | Days before user access snapshots are considered stale |
18+
| DisabledActions | (empty) | Comma-separated list of globally disabled action names |
19+
| DisabledCategories | (empty) | Comma-separated list of globally disabled categories (Core, Management, Advanced) |
1820

1921
## Examples
2022
```powershell
@@ -67,3 +69,33 @@ Get-ROUser -Username 'svc.sim01' -IncludeWeights
6769
```
6870

6971
To change defaults for all new users, edit Data/SeedActionWeights.psd1 before running Add-ROUser.
72+
73+
### Disabling Actions Globally
74+
```powershell
75+
# Disable specific actions for all users
76+
Set-ROConfig -Key 'DisabledActions' -Value 'CreateSecret,CreateFolder'
77+
78+
# Disable an entire category for all users
79+
Set-ROConfig -Key 'DisabledCategories' -Value 'Management'
80+
81+
# Clear (re-enable all)
82+
Set-ROConfig -Key 'DisabledActions' -Value ''
83+
Set-ROConfig -Key 'DisabledCategories' -Value ''
84+
```
85+
86+
Categories: **Core** (SearchSecrets, ViewSecret, CheckoutPassword, ListFolderSecrets, BrowseFolders), **Management** (CreateFolder, CreateSecret, EditSecret, MoveSecret, ToggleComment, ToggleCheckout, ExpireSecret), **Advanced** (CheckinSecret, RunReport, AddFavorite, TriggerHeartbeat, ViewSecretPolicy, ChangePassword, LaunchSecret).
87+
88+
Global disables take precedence over per-user weights.
89+
90+
### Disabling Actions Per User
91+
```powershell
92+
# Disable an entire category for one user
93+
Set-ROUser -Username 'svc.sim01' -DisableCategory 'Management'
94+
95+
# Re-enable (restores default weights)
96+
Set-ROUser -Username 'svc.sim01' -EnableCategory 'Management'
97+
98+
# Disable/enable a single action
99+
Set-ROUser -Username 'svc.sim01' -DisableAction 'CreateSecret'
100+
Set-ROUser -Username 'svc.sim01' -EnableAction 'CreateSecret'
101+
```

Private/Engine/Select-ROUserActions.ps1

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,37 @@ function Select-ROUserActions {
99
[int]$MaxActions = 15
1010
)
1111

12-
# Get user's action weights
12+
# Get user's action weights (weight > 0 means enabled at user level)
1313
$weights = Invoke-ROQuery -Query "SELECT ActionName, Weight FROM ActionWeight WHERE UserId = @UserId AND Weight > 0" -SqlParameters @{ UserId = $UserId }
1414

1515
if (-not $weights) {
1616
Write-ROLog -Message "No action weights found for UserId $UserId" -Level WARN -Component 'Engine'
1717
return @()
1818
}
1919

20+
# Apply global disabled actions filter
21+
$disabledActions = Get-ROConfig -Key 'DisabledActions'
22+
if ($disabledActions) {
23+
$disabledList = $disabledActions -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
24+
$weights = @($weights | Where-Object { $_.ActionName -notin $disabledList })
25+
}
26+
27+
# Apply global disabled categories filter
28+
$disabledCategories = Get-ROConfig -Key 'DisabledCategories'
29+
if ($disabledCategories) {
30+
$disabledCatList = $disabledCategories -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
31+
$registry = Get-ROActionRegistry
32+
$weights = @($weights | Where-Object {
33+
$entry = $registry[$_.ActionName]
34+
-not $entry -or $entry.Category -notin $disabledCatList
35+
})
36+
}
37+
38+
if (-not $weights -or $weights.Count -eq 0) {
39+
Write-ROLog -Message "No eligible actions for UserId $UserId after global filters" -Level WARN -Component 'Engine'
40+
return @()
41+
}
42+
2043
# Determine number of actions for this cycle
2144
$actionCount = Get-Random -Minimum $MinActions -Maximum ($MaxActions + 1)
2245

Public/Initialize-RODatabase.ps1

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ function Initialize-RODatabase {
7070
AuthFailureAction = 'AlertOnly'
7171
LauncherTemplateId = '6052'
7272
AccessSnapshotMaxAgeDays = '7'
73+
DisabledActions = ''
74+
DisabledCategories = ''
7375
}
7476

7577
foreach ($kv in $defaults.GetEnumerator()) {
@@ -110,6 +112,20 @@ function Initialize-RODatabase {
110112
Write-ROLog -Message 'Seeded AccessSnapshotMaxAgeDays config (default: 7)' -Component 'Database'
111113
}
112114

115+
# Ensure DisabledActions config exists (for existing DBs)
116+
$disActCfg = Invoke-ROQuery -Query "SELECT Value FROM Config WHERE Key = 'DisabledActions'" -Scalar
117+
if ($null -eq $disActCfg) {
118+
Invoke-ROQuery -Query "INSERT INTO Config (Key, Value) VALUES ('DisabledActions', '')"
119+
Write-ROLog -Message 'Seeded DisabledActions config (default: empty)' -Component 'Database'
120+
}
121+
122+
# Ensure DisabledCategories config exists (for existing DBs)
123+
$disCatCfg = Invoke-ROQuery -Query "SELECT Value FROM Config WHERE Key = 'DisabledCategories'" -Scalar
124+
if ($null -eq $disCatCfg) {
125+
Invoke-ROQuery -Query "INSERT INTO Config (Key, Value) VALUES ('DisabledCategories', '')"
126+
Write-ROLog -Message 'Seeded DisabledCategories config (default: empty)' -Component 'Database'
127+
}
128+
113129
# Migrate: backfill missing action weights for existing users
114130
$seedWeightPath = Join-Path $PSScriptRoot '..\Data\SeedActionWeights.psd1'
115131
$seedWeightPath = [System.IO.Path]::GetFullPath($seedWeightPath)

Public/Set-ROConfig.ps1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ function Set-ROConfig {
88
Valid config keys: SecretServerUrl, DefaultDomain,
99
MinActionsPerCycle, MaxActionsPerCycle, LogRetentionDays,
1010
PasswordRotationDays, AuthFailureAction (AlertOnly or
11-
RotateAndAlert), LauncherTemplateId, AccessSnapshotMaxAgeDays.
11+
RotateAndAlert), LauncherTemplateId, AccessSnapshotMaxAgeDays,
12+
DisabledActions (comma-separated), DisabledCategories
13+
(comma-separated: Core, Management, Advanced).
1214
.PARAMETER Key
1315
The config key to set.
1416
.PARAMETER Value
@@ -30,6 +32,7 @@ function Set-ROConfig {
3032
[string]$Key,
3133

3234
[Parameter(Mandatory)]
35+
[AllowEmptyString()]
3336
[string]$Value
3437
)
3538

Public/Set-ROUser.ps1

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ function Set-ROUser {
2525
Hashtable of ActionName=Weight pairs to upsert.
2626
.PARAMETER RandomPassword
2727
Generate a random password and set it in both AD and SQLite.
28+
.PARAMETER EnableCategory
29+
Enable all actions in a category (Core, Management, Advanced) by restoring default weights.
30+
.PARAMETER DisableCategory
31+
Disable all actions in a category by setting their weights to 0.
32+
.PARAMETER EnableAction
33+
Enable a specific action by restoring its default weight.
34+
.PARAMETER DisableAction
35+
Disable a specific action by setting its weight to 0.
2836
.EXAMPLE
2937
Set-ROUser -Username 'svc.sim01' -ActiveHourEnd '19:00'
3038
.EXAMPLE
@@ -35,6 +43,15 @@ function Set-ROUser {
3543
.EXAMPLE
3644
Set-ROUser -Username 'svc.sim01' -IsEnabled $false
3745
Disable the user.
46+
.EXAMPLE
47+
Set-ROUser -Username 'svc.sim01' -DisableCategory 'Management'
48+
Disable all Management actions for the user.
49+
.EXAMPLE
50+
Set-ROUser -Username 'svc.sim01' -EnableCategory 'Management'
51+
Restore default weights for all Management actions.
52+
.EXAMPLE
53+
Set-ROUser -Username 'svc.sim01' -DisableAction 'CreateSecret'
54+
Disable a single action for the user.
3855
.OUTPUTS
3956
PSCustomObject - the updated user record
4057
.LINK
@@ -57,7 +74,17 @@ function Set-ROUser {
5774

5875
[hashtable]$ActionWeights,
5976

60-
[switch]$RandomPassword
77+
[switch]$RandomPassword,
78+
79+
[ValidateSet('Core', 'Management', 'Advanced')]
80+
[string]$EnableCategory,
81+
82+
[ValidateSet('Core', 'Management', 'Advanced')]
83+
[string]$DisableCategory,
84+
85+
[string]$EnableAction,
86+
87+
[string]$DisableAction
6188
)
6289

6390
$user = Invoke-ROQuery -Query "SELECT * FROM ROUser WHERE Username = @Username COLLATE NOCASE" -SqlParameters @{ Username = $Username }
@@ -130,5 +157,67 @@ ON CONFLICT(UserId, ActionName) DO UPDATE SET Weight = @Weight
130157
Write-ROLog -Message "Updated action weights for '$Username'" -Component 'UserMgmt'
131158
}
132159

160+
# Category-level enable/disable
161+
if ($EnableCategory -or $DisableCategory) {
162+
$registry = Get-ROActionRegistry
163+
$seedPath = Join-Path $PSScriptRoot '..\Data\SeedActionWeights.psd1'
164+
$seedPath = [System.IO.Path]::GetFullPath($seedPath)
165+
$seedWeights = if (Test-Path $seedPath) { Invoke-Expression (Get-Content -Path $seedPath -Raw) } else { @{} }
166+
167+
$targetCategory = if ($EnableCategory) { $EnableCategory } else { $DisableCategory }
168+
$actionsInCategory = $registry.GetEnumerator() | Where-Object { $_.Value.Category -eq $targetCategory }
169+
170+
foreach ($entry in $actionsInCategory) {
171+
$newWeight = if ($EnableCategory) {
172+
if ($seedWeights[$entry.Key]) { $seedWeights[$entry.Key] } else { 10 }
173+
} else { 0 }
174+
175+
Invoke-ROQuery -Query @"
176+
INSERT INTO ActionWeight (UserId, ActionName, Weight) VALUES (@UserId, @ActionName, @Weight)
177+
ON CONFLICT(UserId, ActionName) DO UPDATE SET Weight = @Weight
178+
"@ -SqlParameters @{
179+
UserId = $user.UserId
180+
ActionName = $entry.Key
181+
Weight = $newWeight
182+
}
183+
}
184+
185+
$verb = if ($EnableCategory) { 'Enabled' } else { 'Disabled' }
186+
Write-ROLog -Message "$verb category '$targetCategory' for '$Username'" -Component 'UserMgmt'
187+
}
188+
189+
# Granular action enable/disable
190+
if ($EnableAction -or $DisableAction) {
191+
$targetAction = if ($EnableAction) { $EnableAction } else { $DisableAction }
192+
$registry = Get-ROActionRegistry
193+
194+
if (-not $registry.ContainsKey($targetAction)) {
195+
Write-Error "Unknown action '$targetAction'. Valid actions: $($registry.Keys -join ', ')"
196+
return
197+
}
198+
199+
$newWeight = 0
200+
if ($EnableAction) {
201+
$seedPath = Join-Path $PSScriptRoot '..\Data\SeedActionWeights.psd1'
202+
$seedPath = [System.IO.Path]::GetFullPath($seedPath)
203+
if (Test-Path $seedPath) {
204+
$seedWeights = Invoke-Expression (Get-Content -Path $seedPath -Raw)
205+
$newWeight = if ($seedWeights[$targetAction]) { $seedWeights[$targetAction] } else { 10 }
206+
} else { $newWeight = 10 }
207+
}
208+
209+
Invoke-ROQuery -Query @"
210+
INSERT INTO ActionWeight (UserId, ActionName, Weight) VALUES (@UserId, @ActionName, @Weight)
211+
ON CONFLICT(UserId, ActionName) DO UPDATE SET Weight = @Weight
212+
"@ -SqlParameters @{
213+
UserId = $user.UserId
214+
ActionName = $targetAction
215+
Weight = $newWeight
216+
}
217+
218+
$verb = if ($EnableAction) { 'Enabled' } else { 'Disabled' }
219+
Write-ROLog -Message "$verb action '$targetAction' for '$Username'" -Component 'UserMgmt'
220+
}
221+
133222
Get-ROUser -Username $Username
134223
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ All settings are stored in the Config SQLite table. Key settings:
9393
| MaxActionsPerCycle | 15 | Max actions per user per cycle |
9494
| PasswordRotationDays | 14 | Days between password rotations |
9595

96-
See [Configuration](Docs/configuration.md) for the full list of 9 config keys and 19 action weight customizations.
96+
See [Configuration](Docs/configuration.md) for the full list of 11 config keys, action weight customizations, and category controls.
9797

9898
## Troubleshooting
9999

RobOtters.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@{
22
RootModule = 'RobOtters.psm1'
3-
ModuleVersion = '0.4.0'
3+
ModuleVersion = '0.5.0'
44
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
55
Author = 'jagger'
66
Description = 'Secret Server user activity simulator for lab environments'

0 commit comments

Comments
 (0)