cascades: CS-SERVER preflight verified + Synology discovery complete
CS-SERVER post-reboot verification: time sync, TLS 1.2 enforcement, and Windows Server Backup feature all persisted cleanly. dcdiag clean. Ready for Entra Connect install. Synology cascadesDS permission inventory captured via DSM API (SSH disabled by default on Synology). 35 users, 4 groups, 10 shares. Analysis identifies 7 shared-account role logins (HIPAA violation), 8 departed-employee accounts to clean up, and 4 shares needing Meredith-side confirmation before migration (pacs most sensitive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""Parse the raw Synology DSM API discovery dump into a clean inventory doc.
|
||||
|
||||
Input: docs/migration/synology-permission-inventory-raw.md
|
||||
Output: docs/migration/synology-permission-inventory.md (clean digest + mapping table)
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
RAW = Path('clients/cascades-tucson/docs/migration/synology-permission-inventory-raw.md')
|
||||
OUT = Path('clients/cascades-tucson/docs/migration/synology-permission-inventory.md')
|
||||
|
||||
|
||||
def extract_json_after(label, text):
|
||||
"""Find the first JSON object after '--- <label> ---' marker using bracket-matching."""
|
||||
idx = text.find(f'--- {label} ---')
|
||||
if idx < 0:
|
||||
return None
|
||||
start = text.find('{', idx)
|
||||
if start < 0:
|
||||
return None
|
||||
depth = 0
|
||||
in_str = False
|
||||
esc = False
|
||||
for i in range(start, len(text)):
|
||||
c = text[i]
|
||||
if esc:
|
||||
esc = False
|
||||
continue
|
||||
if c == "\\":
|
||||
esc = True
|
||||
continue
|
||||
if c == '"':
|
||||
in_str = not in_str
|
||||
continue
|
||||
if in_str:
|
||||
continue
|
||||
if c == '{':
|
||||
depth += 1
|
||||
elif c == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[start:i + 1]
|
||||
return None
|
||||
|
||||
|
||||
def find_all_json_after(label_pattern, text):
|
||||
"""Find every JSON block whose label matches the regex pattern."""
|
||||
results = []
|
||||
for m in re.finditer(r'--- (' + label_pattern + r') ---\n', text):
|
||||
label = m.group(1)
|
||||
body = extract_json_after(label, text[m.start():])
|
||||
if body is None:
|
||||
continue
|
||||
try:
|
||||
results.append((label, json.loads(body)))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
content = RAW.read_text(encoding='utf-8')
|
||||
content = re.sub(r'\n{2,}', '\n', content)
|
||||
|
||||
# USERS
|
||||
users_obj = json.loads(extract_json_after('SYNO.Core.User list', content))
|
||||
users = users_obj.get('data', {}).get('users', [])
|
||||
|
||||
# GROUPS
|
||||
groups_obj = json.loads(extract_json_after('SYNO.Core.Group list', content))
|
||||
groups = groups_obj.get('data', {}).get('groups', [])
|
||||
|
||||
# SHARES
|
||||
shares_obj = json.loads(extract_json_after('SYNO.Core.Share list', content))
|
||||
shares = shares_obj.get('data', {}).get('shares', [])
|
||||
|
||||
# PER-SHARE PERMISSIONS
|
||||
perms = find_all_json_after(r"share '[^']+' - local (?:user|group) permissions", content)
|
||||
|
||||
# Build a lookup: share_name -> {'users': {name: flags}, 'groups': {name: flags}}
|
||||
share_perms = {}
|
||||
for label, obj in perms:
|
||||
m = re.match(r"share '([^']+)' - local (user|group) permissions", label)
|
||||
if not m:
|
||||
continue
|
||||
sname, scope = m.group(1), m.group(2)
|
||||
share_perms.setdefault(sname, {'user': {}, 'group': {}})
|
||||
for item in obj.get('data', {}).get('items', []):
|
||||
nm = item.get('name', '?')
|
||||
share_perms[sname][scope][nm] = {
|
||||
'admin': item.get('is_admin', False),
|
||||
'deny': item.get('is_deny', False),
|
||||
'ro': item.get('is_readonly', False),
|
||||
'rw': item.get('is_writable', False),
|
||||
'custom': item.get('is_custom', False),
|
||||
}
|
||||
|
||||
# Effective write-access table: for each share, who can actually write (writable OR admin) AND not denied
|
||||
def effective(flags):
|
||||
if flags['deny']:
|
||||
return 'DENY'
|
||||
if flags['rw']:
|
||||
return 'RW'
|
||||
if flags['admin']:
|
||||
return 'Admin(full)'
|
||||
if flags['ro']:
|
||||
return 'RO'
|
||||
return '-'
|
||||
|
||||
# Write the digest
|
||||
out_lines = []
|
||||
out_lines.append('# Synology cascadesDS — Permission Inventory (2026-04-22 via DSM API)')
|
||||
out_lines.append('')
|
||||
out_lines.append('**Source:** `docs/migration/synology-permission-inventory-raw.md`')
|
||||
out_lines.append('**Method:** DSM HTTP API v7 via CS-SERVER GuruRMM agent (SSH not enabled on Synology as of 2026-04-22)')
|
||||
out_lines.append('')
|
||||
out_lines.append('## Summary counts')
|
||||
out_lines.append('')
|
||||
out_lines.append(f'- Synology local users: **{len(users)}**')
|
||||
out_lines.append(f'- Synology local groups: **{len(groups)}**')
|
||||
out_lines.append(f'- Shares: **{len(shares)}**')
|
||||
out_lines.append('')
|
||||
|
||||
# Users
|
||||
out_lines.append('## Users')
|
||||
out_lines.append('')
|
||||
out_lines.append('| Name | UID | Expired | Email | Description | Groups |')
|
||||
out_lines.append('|---|---:|---|---|---|---|')
|
||||
for u in sorted(users, key=lambda x: x.get('name', '').lower()):
|
||||
name = u.get('name', '?')
|
||||
uid = u.get('uid', '-')
|
||||
expired = 'yes' if u.get('expired') in ('now', 'expired', True) else 'no'
|
||||
email = u.get('email', '') or ''
|
||||
desc = (u.get('description', '') or '').replace('|', r'\|')
|
||||
groups_list = u.get('additional', {}).get('groups', []) if isinstance(u.get('additional'), dict) else []
|
||||
groups_str = ', '.join(groups_list) if groups_list else '-'
|
||||
out_lines.append(f'| `{name}` | {uid} | {expired} | {email} | {desc} | {groups_str} |')
|
||||
out_lines.append('')
|
||||
|
||||
# Groups
|
||||
out_lines.append('## Groups')
|
||||
out_lines.append('')
|
||||
out_lines.append('| Name | GID | Type | Description | Members |')
|
||||
out_lines.append('|---|---:|---|---|---|')
|
||||
for g in groups:
|
||||
name = g.get('name', '?')
|
||||
gid = g.get('gid', '-')
|
||||
gtype = g.get('type', '-') or '-'
|
||||
desc = g.get('description', '') or ''
|
||||
members = g.get('additional', {}).get('members', []) if isinstance(g.get('additional'), dict) else []
|
||||
member_names = ', '.join([m.get('name', '?') if isinstance(m, dict) else str(m) for m in members]) or '(unable to enumerate via API — error 3201)'
|
||||
out_lines.append(f'| `{name}` | {gid} | {gtype} | {desc} | {member_names} |')
|
||||
out_lines.append('')
|
||||
out_lines.append('> Note: DSM API `SYNO.Core.Group.Member` returned error 3201 for all groups with this API path; membership was only partially available via the `members` additional field on the group list call. Full membership requires DSM web UI or CLI `synogroup --get <name>` via SSH.')
|
||||
out_lines.append('')
|
||||
|
||||
# Shares + permissions
|
||||
out_lines.append('## Shares')
|
||||
out_lines.append('')
|
||||
out_lines.append('| Share | Volume Path | Hidden | Description |')
|
||||
out_lines.append('|---|---|---|---|')
|
||||
for s in shares:
|
||||
name = s.get('name', '?')
|
||||
vol = s.get('vol_path', '') or s.get('additional', {}).get('vol_path', '') or ''
|
||||
hidden = s.get('additional', {}).get('hidden', False) if isinstance(s.get('additional'), dict) else False
|
||||
desc = (s.get('additional', {}).get('desc', '') or '') if isinstance(s.get('additional'), dict) else ''
|
||||
out_lines.append(f'| `{name}` | {vol} | {hidden} | {desc} |')
|
||||
out_lines.append('')
|
||||
|
||||
# Effective permissions per share (write-capable or explicit-deny users/groups)
|
||||
out_lines.append('## Effective share permissions')
|
||||
out_lines.append('')
|
||||
out_lines.append('"Admin(full)" means the account inherits full control via administrators-group membership. "RW" means explicitly writable. "DENY" means explicitly denied (wins over everything else in Synology ACL order). Blank / absent means no explicit permission (effectively no access beyond what the users group grants).')
|
||||
out_lines.append('')
|
||||
|
||||
# Identity sets to call out
|
||||
known_current_employees = {
|
||||
'Ashley Jensen', 'CasAdmin201', 'ChristinaDupras', 'Crystal Rodriguez',
|
||||
'Crystal Suszek', 'Dining Manager', 'JD Martin', 'John Trozzi',
|
||||
'Karen Rossini', 'Lois Lane', 'Lupe Sanchez', 'Megan Hiatt',
|
||||
'meredith kuhn', 'Shelby Trozzi', 'Stephanie Devin', 'Susan Hicks',
|
||||
'Veronica'
|
||||
}
|
||||
known_departed = {
|
||||
'Amber M Lee', 'Ann Dery', 'Anna Pitzlin', 'Britney Thompson',
|
||||
'Haris Durut', 'Monica RamirezRossette', 'Nela Durut-Azizi',
|
||||
'Tamra Johnson'
|
||||
}
|
||||
role_accounts = {
|
||||
'Accounting', 'Dining Manager', 'Front Desk', 'Memcare Receptionist',
|
||||
'mcnurse', 'memcarenurse', 'Nurse Tower'
|
||||
}
|
||||
service_or_admin = {'admin', 'guest', 'guru', 'VPNClient'}
|
||||
|
||||
for sname in sorted(share_perms.keys()):
|
||||
out_lines.append(f'### Share: `{sname}`')
|
||||
out_lines.append('')
|
||||
|
||||
# Groups first
|
||||
g_perms = share_perms[sname]['group']
|
||||
out_lines.append('**Groups:**')
|
||||
out_lines.append('')
|
||||
out_lines.append('| Group | Effective |')
|
||||
out_lines.append('|---|---|')
|
||||
for gname in sorted(g_perms.keys()):
|
||||
out_lines.append(f'| `{gname}` | {effective(g_perms[gname])} |')
|
||||
out_lines.append('')
|
||||
|
||||
# Users with non-default effective permission
|
||||
u_perms = share_perms[sname]['user']
|
||||
nondefault = {n: f for n, f in u_perms.items()
|
||||
if f['rw'] or f['ro'] or f['deny'] or f['admin']}
|
||||
if nondefault:
|
||||
out_lines.append('**Users with explicit permission:**')
|
||||
out_lines.append('')
|
||||
out_lines.append('| User | Effective | Status |')
|
||||
out_lines.append('|---|---|---|')
|
||||
for n in sorted(nondefault.keys(), key=lambda x: x.lower()):
|
||||
eff = effective(nondefault[n])
|
||||
if n in known_departed:
|
||||
status = 'DEPARTED'
|
||||
elif n in role_accounts:
|
||||
status = 'ROLE (HIPAA concern)'
|
||||
elif n in service_or_admin:
|
||||
status = 'service/admin'
|
||||
elif n in known_current_employees:
|
||||
status = 'current'
|
||||
else:
|
||||
status = '?'
|
||||
out_lines.append(f'| `{n}` | {eff} | {status} |')
|
||||
out_lines.append('')
|
||||
else:
|
||||
out_lines.append('*(No user-level overrides — share access is group-based only.)*')
|
||||
out_lines.append('')
|
||||
|
||||
OUT.write_text('\n'.join(out_lines), encoding='utf-8')
|
||||
print(f'Wrote {OUT}')
|
||||
print(f'Bytes: {OUT.stat().st_size}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,166 @@
|
||||
# ============================================================================
|
||||
# Synology Permission Discovery via DSM API - cascadesDS (192.168.0.120)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Runs on CS-SERVER via GuruRMM. Uses DSM's HTTP API (port 5000) instead of
|
||||
# SSH since SSH is not enabled on this Synology.
|
||||
#
|
||||
# Strictly read-only. Login -> list users/groups/shares -> per-share
|
||||
# permissions -> logout.
|
||||
#
|
||||
# Prepared: 2026-04-22 (Cascades Phase 4 Synology retirement prep)
|
||||
# ============================================================================
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$SynoBase = 'http://192.168.0.120:5000'
|
||||
$SynoUser = 'admin'
|
||||
$SynoPass = '__SYNO_PASSWORD__'
|
||||
|
||||
function Section($n) {
|
||||
Write-Output ''
|
||||
Write-Output ('=' * 72)
|
||||
Write-Output "== $n"
|
||||
Write-Output ('=' * 72)
|
||||
}
|
||||
|
||||
function Dump($label, $obj) {
|
||||
Write-Output ''
|
||||
Write-Output "--- $label ---"
|
||||
try {
|
||||
$obj | ConvertTo-Json -Depth 8 | Write-Output
|
||||
} catch {
|
||||
$obj | Format-List * | Out-String | Write-Output
|
||||
}
|
||||
}
|
||||
|
||||
# TLS changes queued for tonight but not yet effective (reboot @ 18:00). This
|
||||
# talks HTTP port 5000 so TLS state doesn't matter here.
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 0: API version discovery'
|
||||
# ----------------------------------------------------------------------------
|
||||
try {
|
||||
$info = Invoke-RestMethod -Uri "$SynoBase/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth,SYNO.Core.User,SYNO.Core.Group,SYNO.Core.Share,SYNO.Core.Share.Permission,SYNO.Core.Group.Member"
|
||||
Dump 'SYNO.API.Info' $info
|
||||
} catch {
|
||||
Write-Output "API info probe failed: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Derive authenticate path + max version
|
||||
$authPath = $info.data.'SYNO.API.Auth'.path
|
||||
$authMax = $info.data.'SYNO.API.Auth'.maxVersion
|
||||
Write-Output "auth path: $authPath, maxVersion: $authMax"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 1: Login'
|
||||
# ----------------------------------------------------------------------------
|
||||
$loginUri = "$SynoBase/webapi/$authPath" +
|
||||
"?api=SYNO.API.Auth&version=$authMax&method=login" +
|
||||
"&account=$([uri]::EscapeDataString($SynoUser))" +
|
||||
"&passwd=$([uri]::EscapeDataString($SynoPass))" +
|
||||
"&session=FileStation&format=sid"
|
||||
try {
|
||||
$loginResp = Invoke-RestMethod -Uri $loginUri
|
||||
Dump 'login response' $loginResp
|
||||
if (-not $loginResp.success) {
|
||||
Write-Output "LOGIN FAILED: $($loginResp | ConvertTo-Json -Depth 5)"
|
||||
exit 1
|
||||
}
|
||||
$sid = $loginResp.data.sid
|
||||
Write-Output "sid: $sid"
|
||||
} catch {
|
||||
Write-Output "Login exception: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Step 2: Users'
|
||||
# ------------------------------------------------------------------------
|
||||
# SYNO.Core.User list with all additional fields
|
||||
$userResp = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.User&version=1&method=list" +
|
||||
"&offset=0&limit=500&type=local" +
|
||||
"&additional=%5B%22email%22%2C%22description%22%2C%22expired%22%2C%22cannot_chg_passwd%22%2C%22passwd_never_expire%22%2C%22passwd_last_change%22%2C%22passwd_must_change%22%2C%22groups%22%5D" +
|
||||
"&_sid=$sid")
|
||||
Dump 'SYNO.Core.User list' $userResp
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Step 3: Groups'
|
||||
# ------------------------------------------------------------------------
|
||||
$groupResp = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.Group&version=1&method=list" +
|
||||
"&offset=0&limit=500&type=local" +
|
||||
"&additional=%5B%22description%22%2C%22group_type%22%2C%22members%22%5D" +
|
||||
"&_sid=$sid")
|
||||
Dump 'SYNO.Core.Group list' $groupResp
|
||||
|
||||
# Group members (separate call in some DSM versions)
|
||||
$groups = $groupResp.data.groups
|
||||
foreach ($g in $groups) {
|
||||
try {
|
||||
$memResp = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.Group.Member&version=1&method=list" +
|
||||
"&group_name=$([uri]::EscapeDataString($g.name))" +
|
||||
"&offset=0&limit=500&_sid=$sid")
|
||||
Dump "members of group '$($g.name)'" $memResp
|
||||
} catch {
|
||||
Write-Output "Group member lookup for '$($g.name)' failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Step 4: Shares'
|
||||
# ------------------------------------------------------------------------
|
||||
$shareResp = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.Share&version=1&method=list" +
|
||||
"&shareType=all&offset=0&limit=200" +
|
||||
"&additional=%5B%22hidden%22%2C%22encryption%22%2C%22share_quota%22%2C%22recyclebin%22%2C%22enable_share_cow%22%2C%22enable_share_compress%22%2C%22name%22%2C%22vol_path%22%2C%22desc%22%5D" +
|
||||
"&_sid=$sid")
|
||||
Dump 'SYNO.Core.Share list' $shareResp
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Step 5: Per-share permissions'
|
||||
# ------------------------------------------------------------------------
|
||||
$shares = $shareResp.data.shares
|
||||
foreach ($sh in $shares) {
|
||||
try {
|
||||
# Permissions by user
|
||||
$permUser = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.Share.Permission&version=1&method=list" +
|
||||
"&name=$([uri]::EscapeDataString($sh.name))" +
|
||||
"&user_group_type=local_user&offset=0&limit=500" +
|
||||
"&_sid=$sid")
|
||||
Dump "share '$($sh.name)' - local user permissions" $permUser
|
||||
# Permissions by group
|
||||
$permGroup = Invoke-RestMethod -Uri ("$SynoBase/webapi/entry.cgi?api=SYNO.Core.Share.Permission&version=1&method=list" +
|
||||
"&name=$([uri]::EscapeDataString($sh.name))" +
|
||||
"&user_group_type=local_group&offset=0&limit=500" +
|
||||
"&_sid=$sid")
|
||||
Dump "share '$($sh.name)' - local group permissions" $permGroup
|
||||
} catch {
|
||||
Write-Output "Permission lookup for '$($sh.name)' failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Step 6: SMB share config (supplementary)'
|
||||
# ------------------------------------------------------------------------
|
||||
# SMB exposure state per share via FileService.SMB.Share or similar
|
||||
try {
|
||||
$smb = Invoke-RestMethod -Uri "$SynoBase/webapi/entry.cgi?api=SYNO.Core.FileServ.SMB&version=1&method=get&_sid=$sid"
|
||||
Dump 'SMB service config' $smb
|
||||
} catch {
|
||||
Write-Output "SMB service config lookup failed: $_"
|
||||
}
|
||||
|
||||
} finally {
|
||||
# ------------------------------------------------------------------------
|
||||
Section 'Logout'
|
||||
# ------------------------------------------------------------------------
|
||||
try {
|
||||
$logout = Invoke-RestMethod -Uri "$SynoBase/webapi/$authPath?api=SYNO.API.Auth&version=$authMax&method=logout&session=FileStation&_sid=$sid"
|
||||
Dump 'logout' $logout
|
||||
} catch {
|
||||
Write-Output "Logout failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
Section 'Done'
|
||||
Write-Output "Completed at $(Get-Date)"
|
||||
@@ -0,0 +1,132 @@
|
||||
# ============================================================================
|
||||
# Synology Permission Discovery - cascadesDS (192.168.0.120)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Runs on CS-SERVER via GuruRMM. Uses plink.exe (PuTTY) to SSH into the
|
||||
# Synology and dump users, groups, share ACLs, and SMB share configs.
|
||||
#
|
||||
# Strictly read-only. No writes, no creates, no config changes.
|
||||
# Output is captured and printed to stdout for the RMM command to return.
|
||||
#
|
||||
# Prepared: 2026-04-22 (Cascades Phase 4 Synology retirement prep)
|
||||
# ============================================================================
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$SynoHost = '192.168.0.120'
|
||||
$SynoUser = 'admin'
|
||||
# Password injected via command substitution at execution time.
|
||||
# Do NOT hardcode here - see the submission wrapper.
|
||||
$SynoPass = '__SYNO_PASSWORD__'
|
||||
|
||||
function Section($n) {
|
||||
Write-Output ''
|
||||
Write-Output ('=' * 72)
|
||||
Write-Output "== $n"
|
||||
Write-Output ('=' * 72)
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 1: Connectivity'
|
||||
# ----------------------------------------------------------------------------
|
||||
$reach = Test-NetConnection -ComputerName $SynoHost -Port 22 -InformationLevel Detailed -WarningAction SilentlyContinue
|
||||
Write-Output "SSH 192.168.0.120:22 reachable: $($reach.TcpTestSucceeded)"
|
||||
if (-not $reach.TcpTestSucceeded) {
|
||||
Write-Output 'ABORT: Cannot reach Synology over SSH. Check pfSense rules + Synology service state.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 2: Locate plink.exe'
|
||||
# ----------------------------------------------------------------------------
|
||||
$plink = $null
|
||||
$candidates = @(
|
||||
'C:\Program Files\PuTTY\plink.exe',
|
||||
'C:\Program Files (x86)\PuTTY\plink.exe',
|
||||
'C:\Tools\plink.exe'
|
||||
)
|
||||
foreach ($c in $candidates) { if (Test-Path $c) { $plink = $c; break } }
|
||||
if (-not $plink) {
|
||||
$plink = (Get-Command plink.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source
|
||||
}
|
||||
if (-not $plink) {
|
||||
Write-Output 'ABORT: plink.exe not found. Install PuTTY or point at a plink.exe path.'
|
||||
exit 1
|
||||
}
|
||||
Write-Output "plink.exe: $plink"
|
||||
$plinkVer = & $plink -V 2>&1 | Select-Object -First 1
|
||||
Write-Output "plink version: $plinkVer"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 3: Pre-accept SSH host key (first run)'
|
||||
# ----------------------------------------------------------------------------
|
||||
# plink with -batch refuses unknown host keys. Pre-accept by piping 'y' into a
|
||||
# non-batch run that does a harmless probe. Subsequent calls use -batch.
|
||||
$preaccept = 'y' | & $plink -ssh -pw $SynoPass "$SynoUser@$SynoHost" 'echo preaccept-ok' 2>&1
|
||||
Write-Output ($preaccept -join "`n")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Step 4: Synology probe - identity + shares + ACLs'
|
||||
# ----------------------------------------------------------------------------
|
||||
# Single heredoc-style payload: one SSH session, captures everything.
|
||||
# synogroup/synouser/synoacltool/synoshare often need sudo on DSM 7; admin is
|
||||
# in administrators group but sudo may still prompt. Use 'sudo -S' with the
|
||||
# password piped via echo to handle the prompt gracefully.
|
||||
$probe = @'
|
||||
PASS="__SUDO_PASSWORD__"
|
||||
sudo_run() { echo "$PASS" | sudo -S -p '' "$@" 2>&1; }
|
||||
|
||||
echo "### date ###"
|
||||
date
|
||||
echo
|
||||
echo "### dsm version ###"
|
||||
cat /etc.defaults/VERSION 2>&1 || cat /etc/VERSION 2>&1
|
||||
echo
|
||||
echo "### synogroup --list ###"
|
||||
sudo_run synogroup --list
|
||||
echo
|
||||
echo "### synogroup --enum global ###"
|
||||
sudo_run synogroup --enum global
|
||||
echo
|
||||
echo "### synouser --enum all ###"
|
||||
sudo_run synouser --enum all
|
||||
echo
|
||||
echo "### /volume1/ directory listing ###"
|
||||
ls -la /volume1/ 2>&1
|
||||
echo
|
||||
echo "### /etc/samba/smb.share.conf ###"
|
||||
sudo_run cat /etc/samba/smb.share.conf 2>&1 | head -200
|
||||
echo
|
||||
echo "### synoshare --enum all ###"
|
||||
sudo_run synoshare --enum all
|
||||
echo
|
||||
for S in homes Management SalesDept Server chat Public Culinary IT Receptionist directoryshare Marketing; do
|
||||
if [ -d /volume1/$S ]; then
|
||||
echo "### synoshare --get $S ###"
|
||||
sudo_run synoshare --get "$S"
|
||||
echo
|
||||
echo "### synoacltool -get /volume1/$S ###"
|
||||
sudo_run synoacltool -get "/volume1/$S"
|
||||
echo
|
||||
echo "### ls -la /volume1/$S (first 15 entries) ###"
|
||||
ls -la "/volume1/$S" 2>&1 | head -15
|
||||
echo
|
||||
fi
|
||||
done
|
||||
echo "### NET SESSIONS / SMB share access (smbstatus) ###"
|
||||
sudo_run smbstatus -p 2>&1 | head -20
|
||||
echo
|
||||
echo "### EOF ###"
|
||||
'@
|
||||
|
||||
# Splice in the sudo password (same as SSH password for admin account)
|
||||
$probe = $probe -replace '__SUDO_PASSWORD__', [regex]::Escape($SynoPass) -replace '\\/','/'
|
||||
# Proper way: use literal replacement without regex escape tricks
|
||||
$probe = $probe.Replace('__SUDO_PASSWORD__', $SynoPass)
|
||||
|
||||
# Execute via plink; batch mode since host key is now cached
|
||||
$result = $probe | & $plink -ssh -batch -pw $SynoPass "$SynoUser@$SynoHost" 'bash -s' 2>&1
|
||||
$result | ForEach-Object { Write-Output $_ }
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
Section 'Done'
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Output "Completed at $(Get-Date)"
|
||||
Reference in New Issue
Block a user